#include <chrono>
#include <thread>

#include <logger.h>
#include <napi_call.h>
#include <node_api.h>

struct InvokeData {
    napi_async_work asyncWork = nullptr;
    int32_t argv0 = 0;
    bool success = false;
    int32_t target = 0;
    napi_ref callbackRef = nullptr;
    napi_deferred deferred = nullptr;
};

void ReleaseInvokeData(napi_env env, struct InvokeData *invokeData)
{
    if (invokeData->asyncWork != nullptr) {
        napi_delete_async_work(env, invokeData->asyncWork);
    }
    if (invokeData->callbackRef != nullptr) {
        napi_delete_reference(env, invokeData->callbackRef);
    }
    delete invokeData;
}

void AsyncExec(napi_env /* env */, void *data)
{
    auto &invokeData = *reinterpret_cast<struct InvokeData *>(data);
    invokeData.target = invokeData.argv0;
    LOG7SO(DEBUG) << "sleep";
    switch (invokeData.argv0 % 4) {
        case 0:
            std::this_thread::sleep_for(std::chrono::seconds(1));
            invokeData.target *= 2;
            invokeData.success = true;
            break;
        case 1:
            std::this_thread::sleep_for(std::chrono::seconds(3));
            invokeData.target += 3;
            invokeData.success = false;
            break;
        case 2:
            std::this_thread::sleep_for(std::chrono::seconds(2));
            invokeData.target /= 4;
            invokeData.success = true;
            break;
        case 3:
            std::this_thread::sleep_for(std::chrono::seconds(4));
            invokeData.target -= 5;
            invokeData.success = false;
            break;
        default:
            break;
    }
}

void AsyncComplete(napi_env env, napi_status status, void* data)
{
    auto &invokeData = *reinterpret_cast<struct InvokeData *>(data);
    napi_value int32 = nullptr;
    napi_value result = nullptr;
    NAPI_CALL_RET(env, napi_create_int32(env, invokeData.target, &int32),);
    NAPI_CALL_RET(env, napi_coerce_to_object(env, int32, &result),);

    if (invokeData.callbackRef != nullptr) {
        napi_value callback = nullptr;
        NAPI_CALL_RET(env, napi_get_reference_value(env, invokeData.callbackRef, &callback),);

        napi_value argv[] = {nullptr, result};
        NAPI_CALL_RET(env, napi_get_boolean(env, invokeData.success, &argv[0]),);

        napi_value undefined = nullptr;
        NAPI_CALL_RET(env, napi_get_undefined(env, &undefined),);
        NAPI_CALL_RET(env, napi_call_function(env, undefined, callback, 2, argv, nullptr),);
    }

    if (invokeData.deferred != nullptr) {
        if (invokeData.success) {
            NAPI_CALL_RET(env, napi_resolve_deferred(env, invokeData.deferred, result),);
        } else {
            NAPI_CALL_RET(env, napi_reject_deferred(env, invokeData.deferred, result),);
        }
    }

    ReleaseInvokeData(env, &invokeData);
}

napi_value SleepAndCalc(napi_env env, napi_callback_info info)
{
    struct InvokeData *invokeData = new struct InvokeData();
    napi_value asyncName = nullptr;
    NAPI_CALL(env, napi_create_string_utf8(env, "sleepAndCalc", NAPI_AUTO_LENGTH, &asyncName));
    NAPI_CALL(env, napi_create_async_work(env, nullptr, asyncName, AsyncExec, AsyncComplete, invokeData, &invokeData->asyncWork));
    NAPI_CALL(env, napi_queue_async_work(env, invokeData->asyncWork));

    size_t argc = 2;
    napi_value argv[2] = {};
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));

    if (argc <= 0 || argc > 2) {
        NAPI_CALL(env, napi_cancel_async_work(env, invokeData->asyncWork));
        ReleaseInvokeData(env, invokeData);
        NAPI_CALL(env, napi_invalid_arg);
    }

    NAPI_CALL(env, napi_get_value_int32(env, argv[0], &invokeData->argv0));
    if (argc == 1) {
        napi_value promise = nullptr;
        NAPI_CALL(env, napi_create_promise(env, &invokeData->deferred, &promise));
        return promise;
    }

    napi_value global = nullptr;
    NAPI_CALL(env, napi_get_global(env, &global));

    napi_value funcCtor = nullptr;
    NAPI_CALL(env, napi_get_named_property(env, global, "Function", &funcCtor));

    bool instanceof = false;
    NAPI_CALL(env, napi_instanceof(env, argv[1], funcCtor, &instanceof));

    if (!instanceof) {
        NAPI_CALL(env, napi_cancel_async_work(env, invokeData->asyncWork));
        ReleaseInvokeData(env, invokeData);
        NAPI_CALL(env, napi_invalid_arg);
    }

    NAPI_CALL(env, napi_create_reference(env, argv[1], 1, &invokeData->callbackRef));
    return nullptr;
}

/* napi_value */
NAPI_MODULE_INIT(/* napi_env env, napi_value exports */)
{
    napi_value sleepAndCalc;
    NAPI_CALL(env, napi_create_function(env, "sleep_and_calc", NAPI_AUTO_LENGTH, SleepAndCalc, nullptr, &sleepAndCalc));
    NAPI_CALL(env, napi_set_named_property(env, exports, "sleep_and_calc", sleepAndCalc));
    return exports;
}
